/**
 * Copyright (c) 2010-2011 Pierre Labatut
 *
 * This file is part of Space-Chronicles project.
 *
 * Space-Chronicles is free software: you can redistribute it and/or modify it
 * under the terms of its license which may be found in the accompanying
 * LICENSE file or <http://code.google.com/p/space-chronicles/>.
 */
package space.game.world;

import javax.media.opengl.GL2;
import javax.media.opengl.GL3;
import javax.media.opengl.GL3bc;
import jinngine.math.Matrix4;

import space.debug.GlobalParameters;
import space.gl.FrameBufferObject2x2;
import space.gl.RenderContext;
import space.model.RenderingCamera;

/**
 *
 * @author Pierre
 */
public class WldLightCSM {

    public final int SLICE_COUNT = 4;
    
    private FrameBufferObject2x2 FBO;
    private final Matrix4 lightMatrices[] = new Matrix4[SLICE_COUNT];
    private WldLightCSMSlice[] slices;
    private final static int TEX_SIZE = 4096;

    static WldLightCSMSlice[] slices(double fovy, double aspect, double near, double far, int sliceCount, double lambda) {
        /**
         * First update the split locations
         */
        final double splits[] = new double[sliceCount + 1];
        {
            // The splits distance compose a geometrical suite, where he first and
            // last element match the near and far plane
            final double ratio = far / near;
            splits[0] = near;
            for (int i = 1; i < sliceCount; i++) {
                double si = i / (double) sliceCount;
                splits[i] =
                        lambda * (near * Math.pow(ratio, si)) + (1. - lambda) * (near + (far - near) * si);
            }
            splits[sliceCount] = far;
        }


        final WldLightCSMSlice[] slices = new WldLightCSMSlice[sliceCount];
        {
            /**             
             * Now let's find a bounding sphere. 
             */
            double tanfovy = Math.tan(Math.toRadians(fovy * .5));
            double tanfov2 = tanfovy * tanfovy * (aspect * aspect + 1.);

            for (int sliceIdx = 0; sliceIdx < sliceCount; sliceIdx++) {
                final double snear = splits[sliceIdx];
                final double sfar = splits[sliceIdx + 1];
                final double h = (snear + sfar) * .5;
                final double i = h * tanfov2;
                final double center = h + i;
                final double nc = i + (sfar - snear) * .5;
                final double radius = Math.sqrt(
                        nc * nc + tanfov2 * snear * snear);
                slices[sliceIdx] = new WldLightCSMSlice(snear, sfar, center,
                        radius);
            }
        }
        return slices;
    }

    public void renderShadowMap(final RenderContext rc, final Matrix4 lightModification, final RenderingCamera camera, double aspect, WldScene scene) {
        /**
         * Compute viewpoints
         */
        final Matrix4 worldToView = camera.viewpoint();
        final Matrix4 viewToWorld = worldToView.inverse();
        slices = slices(camera.fovy(), aspect, camera.near(), camera.far(), SLICE_COUNT, GlobalParameters.lambda);
        for (int sliceIdx = 0; sliceIdx < SLICE_COUNT; sliceIdx++) {
            lightMatrices[sliceIdx] = slices[sliceIdx].lightMatrix(
                    viewToWorld, lightModification, TEX_SIZE);
        }
        final GL3bc gl = rc.gl();

        FBO.pushFBORenderingState(gl);
        //Clear 4 images of the atlas by removing the scissor test in order to clear the whole buffer
        gl.glDisable(GL3.GL_SCISSOR_TEST);
        gl.glClear(GL3.GL_DEPTH_BUFFER_BIT);


        gl.glPolygonOffset(GlobalParameters.polygonOffsetFactor, GlobalParameters.polygonOffsetUnits);
        gl.glEnable(GL3.GL_POLYGON_OFFSET_FILL);
        gl.glEnable(GL3.GL_SCISSOR_TEST);
        gl.glEnable(GL3.GL_DEPTH_CLAMP);
        for (int sliceIdx = 0; sliceIdx < SLICE_COUNT; sliceIdx++) {
            FBO.expose(gl, sliceIdx);
            if (GlobalParameters.showFunstrumInSM) {// Display the camera frustum
                rc.setViewProjection(lightMatrices[sliceIdx]);
                rc.setWorldView(viewToWorld);
                rc.setObjectWorld(Matrix4.identity());
                rc.applyLegacyMatrix();


                double ty = Math.tan(Math.toRadians(camera.fovy()) * .5);
                double tx = aspect * ty;
                double n = slices[sliceIdx].near;
                double f = slices[sliceIdx].far;

                //Draw the funstrum in view space
                gl.glBegin(gl.GL_LINES);
                gl.glVertex3d(+n * tx, +n * ty, -n);
                gl.glVertex3d(+f * tx, +f * ty, -f);
                gl.glVertex3d(-n * tx, +n * ty, -n);
                gl.glVertex3d(-f * tx, +f * ty, -f);
                gl.glVertex3d(+n * tx, -n * ty, -n);
                gl.glVertex3d(+f * tx, -f * ty, -f);
                gl.glVertex3d(-n * tx, -n * ty, -n);
                gl.glVertex3d(-f * tx, -f * ty, -f);

                gl.glVertex3d(+n * tx, +n * ty, -n);
                gl.glVertex3d(-n * tx, +n * ty, -n);
                gl.glVertex3d(-n * tx, +n * ty, -n);
                gl.glVertex3d(-n * tx, -n * ty, -n);
                gl.glVertex3d(-n * tx, -n * ty, -n);
                gl.glVertex3d(+n * tx, -n * ty, -n);
                gl.glVertex3d(+n * tx, -n * ty, -n);
                gl.glVertex3d(+n * tx, +n * ty, -n);

                gl.glVertex3d(+f * tx, +f * ty, -f);
                gl.glVertex3d(-f * tx, +f * ty, -f);
                gl.glVertex3d(-f * tx, +f * ty, -f);
                gl.glVertex3d(-f * tx, -f * ty, -f);
                gl.glVertex3d(-f * tx, -f * ty, -f);
                gl.glVertex3d(+f * tx, -f * ty, -f);
                gl.glVertex3d(+f * tx, -f * ty, -f);
                gl.glVertex3d(+f * tx, +f * ty, -f);

                gl.glEnd();
            }

            rc.setViewProjection(lightMatrices[sliceIdx]);
            rc.setWorldView(Matrix4.identity());
            rc.setObjectWorld(Matrix4.identity());
            rc.startUsingProgram();
            scene.draw(rc);
            rc.stopUsingProgram();
        }
        FBO.popFBORenderingState(gl);

        gl.glDisable(GL3.GL_DEPTH_CLAMP);
        gl.glDisable(GL3.GL_POLYGON_OFFSET_FILL);
        gl.glDisable(GL3.GL_SCISSOR_TEST);
    }

    /**
     * {@link http://www.opengl.org/sdk/docs/man/xhtml/glOrtho.xml}
     * @param left Specify the coordinates for the left vertical clipping planes.
     * @param right Specify the coordinates for the right vertical clipping planes.
     * @param bottom Specify the coordinates for the bottom horizontal clipping planes.
     * @param top Specify the coordinates for the top horizontal clipping planes.
     * @param nearVal Specify the distances to the nearer depth clipping planes.
     *                This value is negative when the plane is to be behind the viewer.
     * @param farVal Specify the distances to the farther depth clipping planes.
     *               This value is negative when the plane is to be behind the viewer.
     * @return a new allowated matrix storing the orthographic matrix
     */
    static Matrix4 orthographic(double left, double right, double bottom,
            double top, double nearVal, double farVal) {
        return new Matrix4(
                2. / (right - left), 0., 0., -(right + left) / (right - left),
                0., 2. / (top - bottom), 0., -(top + bottom) / (top - bottom),
                0., 0., -2. / (farVal - nearVal),
                -(farVal + nearVal) / (farVal - nearVal),
                0., 0., 0., 1.);

    }

    public WldLightCSM(
            GL3bc gl) {
        FBO = new FrameBufferObject2x2(gl, TEX_SIZE, TEX_SIZE);
    }

    public void dispose(GL3bc gl) {
    }

    public void draw(GL3bc gl) {
        gl.glTexParameteri(GL2.GL_TEXTURE_2D, GL2.GL_TEXTURE_COMPARE_MODE,
                GL2.GL_NONE);
        gl.glTexEnvi(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE,
                GL2.GL_REPLACE);

    }

    public Matrix4[] getTextureCoordMatrix() {
        Matrix4 m0 = new Matrix4(
                .25, .0, .0, .25,
                .0, .25, .0, .25,
                .0, .0, .5, .5,
                .0, .0, .0, 1.);
        Matrix4.multiply(m0, lightMatrices[0], m0);
        Matrix4 m1 = new Matrix4(
                .25, .0, .0, .75,
                .0, .25, .0, .25,
                .0, .0, .5, .5,
                .0, .0, .0, 1.);
        Matrix4.multiply(m1, lightMatrices[1], m1);
        Matrix4 m2 = new Matrix4(
                .25, .0, .0, .25,
                .0, .25, .0, .75,
                .0, .0, .5, .5,
                .0, .0, .0, 1.);
        Matrix4.multiply(m2, lightMatrices[2], m2);
        Matrix4 m3 = new Matrix4(
                .25, .0, .0, .75,
                .0, .25, .0, .75,
                .0, .0, .5, .5,
                .0, .0, .0, 1.);
        Matrix4.multiply(m3, lightMatrices[3], m3);
        return new Matrix4[]{m0, m1, m2, m3};
    }

    public void bindTexture(GL3bc gl) {
        FBO.bindTexture(gl);
    }

    public WldLightCSMSlice[] getSlices() {
        return slices;
    }

    public void debugFBO(GL3bc gl) {
        // The debug viewer is never hidden by objects in the scene
        gl.glDisable(gl.GL_DEPTH_TEST);

        // The viewer is defined in clip coordinates x,y,z in [-1,1]
        gl.glMatrixMode(gl.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glMatrixMode(gl.GL_MODELVIEW);
        gl.glLoadIdentity();

        // Disable eventual additional texture unit
        gl.glActiveTexture(GL2.GL_TEXTURE1);
        gl.glDisable(GL2.GL_TEXTURE_2D);

        // Enable a default texture unit that reaplce the fragment color
        gl.glActiveTexture(GL2.GL_TEXTURE0);
        gl.glEnable(GL2.GL_TEXTURE_2D);
        gl.glTexEnvi(GL2.GL_TEXTURE_ENV, GL2.GL_TEXTURE_ENV_MODE, GL2.GL_REPLACE);

        // Use the FBO bounded texture
        bindTexture(gl);

        // Display a quad with texture coordiantes
        gl.glBegin(gl.GL_QUADS);
        gl.glTexCoord2d(0., 0.);
        gl.glVertex2d(.3, .2);
        gl.glTexCoord2d(1., 0.);
        gl.glVertex2d(.9, .2);
        gl.glTexCoord2d(1., 1.);
        gl.glVertex2d(.9, .9);
        gl.glTexCoord2d(0., 1.);
        gl.glVertex2d(.3, .9);
        gl.glEnd();

        // Clean up texture unit state
        gl.glEnable(gl.GL_DEPTH_TEST);
        gl.glActiveTexture(GL2.GL_TEXTURE0);
        gl.glDisable(GL2.GL_TEXTURE_2D);

    }

    public void setDebuggerVisible(boolean isVisible) {
        FBO.setDebuggerVisible(isVisible);
    }
}
